UsersTable.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. "use client";
  2. import { ColumnDef } from "@tanstack/react-table";
  3. import {
  4. DropdownMenu,
  5. DropdownMenuContent,
  6. DropdownMenuItem,
  7. DropdownMenuTrigger
  8. } from "@app/components/ui/dropdown-menu";
  9. import { Button } from "@app/components/ui/button";
  10. import { ArrowRight, ArrowUpDown, Crown, MoreHorizontal } from "lucide-react";
  11. import { UsersDataTable } from "./UsersDataTable";
  12. import { useState } from "react";
  13. import InviteUserForm from "./InviteUserForm";
  14. import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
  15. import { useOrgContext } from "@app/hooks/useOrgContext";
  16. import { useToast } from "@app/hooks/useToast";
  17. import Link from "next/link";
  18. import { useRouter } from "next/navigation";
  19. import { formatAxiosError } from "@app/lib/utils";
  20. import { createApiClient } from "@app/api";
  21. import { useEnvContext } from "@app/hooks/useEnvContext";
  22. import { useUserContext } from "@app/hooks/useUserContext";
  23. export type UserRow = {
  24. id: string;
  25. email: string;
  26. status: string;
  27. role: string;
  28. isOwner: boolean;
  29. };
  30. type UsersTableProps = {
  31. users: UserRow[];
  32. };
  33. export default function UsersTable({ users: u }: UsersTableProps) {
  34. const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
  35. const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
  36. const [selectedUser, setSelectedUser] = useState<UserRow | null>(null);
  37. const [users, setUsers] = useState<UserRow[]>(u);
  38. const router = useRouter();
  39. const api = createApiClient(useEnvContext());
  40. const { user, updateUser } = useUserContext();
  41. const { org } = useOrgContext();
  42. const { toast } = useToast();
  43. const columns: ColumnDef<UserRow>[] = [
  44. {
  45. id: "dots",
  46. cell: ({ row }) => {
  47. const userRow = row.original;
  48. return (
  49. <>
  50. <div>
  51. {userRow.isOwner && (
  52. <MoreHorizontal className="h-4 w-4 opacity-0" />
  53. )}
  54. {!userRow.isOwner && (
  55. <>
  56. <DropdownMenu>
  57. <DropdownMenuTrigger asChild>
  58. <Button
  59. variant="ghost"
  60. className="h-8 w-8 p-0"
  61. >
  62. <span className="sr-only">
  63. Open menu
  64. </span>
  65. <MoreHorizontal className="h-4 w-4" />
  66. </Button>
  67. </DropdownMenuTrigger>
  68. <DropdownMenuContent align="end">
  69. <DropdownMenuItem>
  70. <Link
  71. href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
  72. className="block w-full"
  73. >
  74. Manage User
  75. </Link>
  76. </DropdownMenuItem>
  77. {userRow.email !== user?.email && (
  78. <DropdownMenuItem
  79. onClick={() => {
  80. setIsDeleteModalOpen(
  81. true
  82. );
  83. setSelectedUser(
  84. userRow
  85. );
  86. }}
  87. >
  88. <span className="text-red-500">
  89. Remove User
  90. </span>
  91. </DropdownMenuItem>
  92. )}
  93. </DropdownMenuContent>
  94. </DropdownMenu>
  95. </>
  96. )}
  97. </div>
  98. </>
  99. );
  100. }
  101. },
  102. {
  103. accessorKey: "email",
  104. header: ({ column }) => {
  105. return (
  106. <Button
  107. variant="ghost"
  108. onClick={() =>
  109. column.toggleSorting(column.getIsSorted() === "asc")
  110. }
  111. >
  112. Email
  113. <ArrowUpDown className="ml-2 h-4 w-4" />
  114. </Button>
  115. );
  116. }
  117. },
  118. {
  119. accessorKey: "status",
  120. header: ({ column }) => {
  121. return (
  122. <Button
  123. variant="ghost"
  124. onClick={() =>
  125. column.toggleSorting(column.getIsSorted() === "asc")
  126. }
  127. >
  128. Status
  129. <ArrowUpDown className="ml-2 h-4 w-4" />
  130. </Button>
  131. );
  132. }
  133. },
  134. {
  135. accessorKey: "role",
  136. header: ({ column }) => {
  137. return (
  138. <Button
  139. variant="ghost"
  140. onClick={() =>
  141. column.toggleSorting(column.getIsSorted() === "asc")
  142. }
  143. >
  144. Role
  145. <ArrowUpDown className="ml-2 h-4 w-4" />
  146. </Button>
  147. );
  148. },
  149. cell: ({ row }) => {
  150. const userRow = row.original;
  151. return (
  152. <div className="flex flex-row items-center gap-1">
  153. {userRow.isOwner && (
  154. <Crown className="w-4 h-4 text-yellow-600" />
  155. )}
  156. <span>{userRow.role}</span>
  157. </div>
  158. );
  159. }
  160. },
  161. {
  162. id: "actions",
  163. cell: ({ row }) => {
  164. const userRow = row.original;
  165. return (
  166. <div className="flex items-center justify-end">
  167. {userRow.isOwner && (
  168. <Button
  169. variant="ghost"
  170. className="opacity-0 cursor-default"
  171. >
  172. Placeholder
  173. </Button>
  174. )}
  175. {!userRow.isOwner && (
  176. <Link
  177. href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
  178. >
  179. <Button variant={"gray"} className="ml-2">
  180. Manage
  181. <ArrowRight className="ml-2 w-4 h-4" />
  182. </Button>
  183. </Link>
  184. )}
  185. </div>
  186. );
  187. }
  188. }
  189. ];
  190. async function removeUser() {
  191. if (selectedUser) {
  192. const res = await api
  193. .delete(`/org/${org!.org.orgId}/user/${selectedUser.id}`)
  194. .catch((e) => {
  195. toast({
  196. variant: "destructive",
  197. title: "Failed to remove user",
  198. description: formatAxiosError(
  199. e,
  200. "An error occurred while removing the user."
  201. )
  202. });
  203. });
  204. if (res && res.status === 200) {
  205. toast({
  206. variant: "default",
  207. title: "User removed",
  208. description: `The user ${selectedUser.email} has been removed from the organization.`
  209. });
  210. setUsers((prev) =>
  211. prev.filter((u) => u.id !== selectedUser?.id)
  212. );
  213. }
  214. }
  215. setIsDeleteModalOpen(false);
  216. }
  217. return (
  218. <>
  219. <ConfirmDeleteDialog
  220. open={isDeleteModalOpen}
  221. setOpen={(val) => {
  222. setIsDeleteModalOpen(val);
  223. setSelectedUser(null);
  224. }}
  225. dialog={
  226. <div className="space-y-4">
  227. <p>
  228. Are you sure you want to remove{" "}
  229. <b>{selectedUser?.email}</b> from the organization?
  230. </p>
  231. <p>
  232. Once removed, this user will no longer have access
  233. to the organization. You can always re-invite them
  234. later, but they will need to accept the invitation
  235. again.
  236. </p>
  237. <p>
  238. To confirm, please type the email address of the
  239. user below.
  240. </p>
  241. </div>
  242. }
  243. buttonText="Confirm Remove User"
  244. onConfirm={removeUser}
  245. string={selectedUser?.email ?? ""}
  246. title="Remove User from Organization"
  247. />
  248. <InviteUserForm
  249. open={isInviteModalOpen}
  250. setOpen={setIsInviteModalOpen}
  251. />
  252. <UsersDataTable
  253. columns={columns}
  254. data={users}
  255. inviteUser={() => {
  256. setIsInviteModalOpen(true);
  257. }}
  258. />
  259. </>
  260. );
  261. }